'    Copyright 2008 Jan Molnar <jan.molnar@abds.de>
'
'    This file is part of JCQ.

'    JCQ is free software: you can redistribute it and/or modify
'    it under the terms of the GNU General Public License as published by
'    the Free Software Foundation, either version 2 of the License, or
'    (at your option) any later version.

'    JCQ is distributed in the hope that it will be useful,
'    but WITHOUT ANY WARRANTY; without even the implied warranty of
'    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
'    GNU General Public License for more details.

'    You should have received a copy of the GNU General Public License
'    along with JCQ. If not, see <http://www.gnu.org/licenses/>.

Imports JCsTools.JCQ.IcqInterface.DataTypes

Public Class BaseConnector
    Inherits ContextService
    Implements IIcqDataTranferService

    Private _TcpContext As TcpContext

    Private ReadOnly _analyzeLock As Object
    Private ReadOnly _analyzeBuffer As List(Of Byte)

    Private ReadOnly _snacHandlers As Dictionary(Of String, List(Of [Delegate]))

    Private _flapSequenceNumber As Integer

    Private ReadOnly _sendLock As Object
    Private ReadOnly _sendBuffer As List(Of FlapDataPair)
    Private ReadOnly _sendTimer As System.Threading.Timer
    Private ReadOnly _sendTimerDue As Integer

    Private _sendTimerRunning As Boolean

    Protected Event InternalConnected As EventHandler
    Protected Event InternalDisconnected As EventHandler(Of DisconnectEventArgs)

    Public Sub New(ByVal context As Interfaces.IContext)
        MyBase.New(context)
        _TcpContext = New TcpContext

        _analyzeLock = New Object()
        _analyzeBuffer = New List(Of Byte)

        _snacHandlers = New Dictionary(Of String, List(Of [Delegate]))

        _sendLock = New Object
        _sendBuffer = New List(Of FlapDataPair)
        _sendTimer = New System.Threading.Timer(AddressOf OnSendTimerTick, Nothing, Threading.Timeout.Infinite, Threading.Timeout.Infinite)
        _sendTimerDue = 100
    End Sub

    Public ReadOnly Property IsConnected() As Boolean
        Get
            Return TcpContext IsNot Nothing AndAlso TcpContext.IsConnected
        End Get
    End Property

    Public ReadOnly Property TcpContext() As TcpContext
        Get
            Return _TcpContext
        End Get
    End Property

    Protected Overridable Sub InnerConnect()
        Dim hostName As String = System.Configuration.ConfigurationManager.AppSettings("OscarServer")
        Dim hostPort As Integer = Integer.Parse(System.Configuration.ConfigurationManager.AppSettings("OscarPort"))

        Dim hentry As System.Net.IPHostEntry = System.Net.Dns.GetHostEntry(hostName)
        Dim endpoint As System.Net.IPEndPoint = New System.Net.IPEndPoint(hentry.AddressList(0), hostPort)

        InnerConnect(endpoint)
    End Sub

    Protected Overridable Sub InnerConnect(ByVal endpoint As System.Net.IPEndPoint)
        InnerDisconnect()

        _TcpContext = New TcpContext

        _TcpContext.Connect(endpoint)

        AddHandler _TcpContext.DataReceived, AddressOf OnTcpContextDataReceived
        AddHandler _TcpContext.Disconnected, AddressOf OnTcpContextDisconnected

        OnInternalConnected(System.EventArgs.Empty)
    End Sub

    Protected Overridable Sub InnerDisconnect()
        If _TcpContext IsNot Nothing Then _TcpContext.Disconnect()
    End Sub

    Protected Overridable Sub OnInternalConnected(ByVal e As EventArgs)
        RaiseEvent InternalConnected(Me, e)
    End Sub

    Protected Overridable Sub OnInternalDisconnected(ByVal e As DisconnectEventArgs)
        RaiseEvent InternalDisconnected(Me, e)
    End Sub

    Protected Function ConvertServerAddressToEndPoint(ByVal address As String) As System.Net.IPEndPoint
        Dim serverAddressParts As String()
        Dim serverIp As System.Net.IPAddress
        Dim serverPort As Integer

        If address Is Nothing Then Throw New ArgumentNullException("address")

        serverAddressParts = address.Split(":"c)

        serverIp = System.Net.IPAddress.Parse(serverAddressParts(0))
        serverPort = Integer.Parse(serverAddressParts(1))

        Return New System.Net.IPEndPoint(serverIp, serverPort)
    End Function

    Private Sub OnTcpContextDisconnected(ByVal sender As Object, ByVal e As DisconnectEventArgs)
        OnInternalDisconnected(e)
    End Sub

#Region " Data Receiving "

    Public Event FlapReceived(ByVal sender As Object, ByVal e As FlapTransportEventArgs) Implements IIcqDataTranferService.FlapReceived

    Private Sub OnTcpContextDataReceived(ByVal sender As Object, ByVal e As DataReceivedEventArgs)
        Dim data As List(Of Byte) = e.Data

        Try
            SyncLock _analyzeLock
                If _analyzeBuffer.Count > 0 Then
                    Core.Kernel.Logger.Log("BaseConnector", TraceEventType.Verbose, "data received. already cached: {0}, new: {1}, ticket: {2}", _analyzeBuffer.Count, data.Count, e.Ticket)
                    data.InsertRange(0, _analyzeBuffer)
                    _analyzeBuffer.Clear()
                End If

                Dim index As Integer = 0
                Dim iloop As Integer = 0

                Do While index + 6 <= data.Count
                    Dim desc As FlapDescriptor = FlapDescriptor.GetDescriptor(index, data)

                    If data.Count < index + desc.TotalSize Then
                        Core.Kernel.Logger.Log("BaseConnector", TraceEventType.Verbose, "{0}: caching {1}, {2} more required.", iloop, data.Count - index, desc.TotalSize)
                        _analyzeBuffer.AddRange(data.GetRange(index, data.Count - index))
                    Else
                        Core.Kernel.Logger.Log("BaseConnector", TraceEventType.Verbose, "{0}: queuing {1} bytes for analyzation", iloop, data.Count - index, desc.TotalSize)
                        System.Threading.ThreadPool.QueueUserWorkItem(New System.Threading.WaitCallback(AddressOf AsyncPreAnalyze), data.GetRange(index, desc.TotalSize))
                    End If

                    index += desc.TotalSize
                    iloop += 1
                Loop
            End SyncLock
        Catch ex As Exception
            JCsTools.Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Sub AsyncPreAnalyze(ByVal state As Object)
        Dim data As List(Of Byte)
        Dim f As Flap

        Try
            data = DirectCast(state, Global.System.Collections.Generic.List(Of Byte))

            f = New Flap
            f.Deserialize(data)
            RaiseEvent FlapReceived(Me, New FlapTransportEventArgs(f))
        Catch ex As Exception
            JCsTools.Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Function ToHex(ByVal data As IEnumerable(Of Byte)) As String
        Dim sb As New System.Text.StringBuilder
        For Each b As Byte In data
            sb.AppendFormat("{0:X2} ", b)
        Next
        Return sb.ToString
    End Function

    Private Sub OnFlapReceived(ByVal sender As Object, ByVal e As FlapTransportEventArgs) Handles Me.FlapReceived
        Dim flap As Flap = e.Flap

        Try
            If flap.Channel = FlapChannel.SnacData Then
                For Each x As Snac In flap.DataItems
                    CallSnacHandlers(x)

                    Core.Kernel.Logger.Log("BaseConnector", TraceEventType.Verbose, "Processed: {0}", Snac.GetKey(x))
                Next
            End If
        Catch ex As Exception
            JCsTools.Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

#End Region

#Region " Data Sending "

    Public Event FlapSent(ByVal sender As Object, ByVal e As FlapTransportEventArgs) Implements IIcqDataTranferService.FlapSent

    Private Sub OnSendTimerTick(ByVal state As Object)
        Dim dataToSend() As FlapDataPair

        Try
            SyncLock _sendLock
                Core.Kernel.Logger.Log("BaseConnector", TraceEventType.Verbose, "Send timer tick {0} items in buffer", _sendBuffer.Count)

                If _sendBuffer.Count = 0 Then Return

                ReDim dataToSend(_sendBuffer.Count - 1)

                _sendBuffer.CopyTo(dataToSend)
                _sendBuffer.Clear()

                _sendTimer.Change(Threading.Timeout.Infinite, Threading.Timeout.Infinite)
                _sendTimerRunning = False
            End SyncLock

            For Each dataItem In dataToSend
                _TcpContext.SendData(dataItem.Data)

                RaiseEvent FlapSent(Me, New FlapTransportEventArgs(dataItem.Flap))
            Next
        Catch ex As Exception
            Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

    Private Sub AddItemToSendBuffer(ByVal flap As Flap)
        SyncLock _sendLock
            _sendBuffer.Add(New FlapDataPair(flap))

            If Not _sendTimerRunning Then
                _sendTimer.Change(_sendTimerDue, Threading.Timeout.Infinite)
                _sendTimerRunning = True
            End If
        End SyncLock
    End Sub

    Private Sub AddItemsToSendBuffer(ByVal flaps As IEnumerable(Of Flap))
        SyncLock _sendLock
            _sendBuffer.AddRange(From x In flaps Select New FlapDataPair(x))

            If Not _sendTimerRunning Then
                _sendTimer.Change(_sendTimerDue, Threading.Timeout.Infinite)
                _sendTimerRunning = True
            End If
        End SyncLock
    End Sub

    Public Sub Send(ByVal flap As DataTypes.Flap) Implements IIcqDataTranferService.Send
        If Not IsConnected Then Throw New InvalidOperationException("Invalid try to send data. TcpContext is not connected.")

        '_flapSequenceNumber += 1

        flap.DatagramSequenceNumber = Threading.Interlocked.Increment(_flapSequenceNumber)

        '_TcpContext.SendData(flap.Serialize)

        'RaiseEvent FlapSent(Me, New FlapTransportEventArgs(flap))

        AddItemToSendBuffer(flap)
    End Sub

    Public Sub Send(ByVal ParamArray snacs() As DataTypes.Snac) Implements IIcqDataTranferService.Send
        SendList(snacs)
    End Sub

    Public Sub SendList(ByVal snacs As System.Collections.Generic.IEnumerable(Of DataTypes.Snac)) Implements IIcqDataTranferService.SendList
        Dim flap As DataTypes.Flap
        'Dim data As List(Of Byte) = New List(Of Byte)
        Dim dataItems As List(Of Flap)

        If Not IsConnected Then Throw New InvalidOperationException("Invalid try to send data. TcpContext is not connected.")

        dataItems = New List(Of Flap)

        For Each x As Snac In snacs
            flap = New DataTypes.Flap(FlapChannel.SnacData)

            '_flapSequenceNumber += 1

            flap.DatagramSequenceNumber = Threading.Interlocked.Increment(_flapSequenceNumber)
            flap.DataItems.Add(x)

            'data.AddRange(flap.Serialize)
            dataItems.Add(flap)

            'RaiseEvent FlapSent(Me, New FlapTransportEventArgs(flap))
        Next

        '_TcpContext.SendData(data)

        AddItemsToSendBuffer(dataItems)
    End Sub

#End Region

    Public Sub RegisterSnacHandler(Of T As DataTypes.Snac)(ByVal serviceId As Integer, ByVal subtypeId As Integer, ByVal handler As System.Action(Of T)) Implements IIcqDataTranferService.RegisterSnacHandler
        Dim key As String
        Dim handlerList As List(Of [Delegate]) = Nothing

        key = String.Format("{0:X2},{1:X2}", serviceId, subtypeId)

        If Not _snacHandlers.TryGetValue(key, handlerList) Then
            handlerList = New List(Of [Delegate])

            _snacHandlers.Add(key, handlerList)
        End If

        handlerList.Add(handler)
    End Sub

    Private Sub CallSnacHandlers(ByVal snac As Snac)
        Dim key As String = snac.GetKey(snac)
        Dim handlers As List(Of [Delegate]) = Nothing

        Try
            Debug.WriteLine(String.Format(">> Snac {0}", key), "BaseConnector")

            If _snacHandlers.TryGetValue(key, handlers) Then
                For Each x As [Delegate] In handlers
                    x.DynamicInvoke(snac)
                Next
            End If
        Catch ex As Exception
            Core.Kernel.Exceptions.PublishException(ex)
        End Try
    End Sub

End Class

Public Class FlapDataPair
    Public Sub New(ByVal f As Flap)
        _Flap = f
        _Data = f.Serialize
    End Sub

    Private ReadOnly _Flap As Flap
    Public ReadOnly Property Flap() As Flap
        Get
            Return _Flap
        End Get
    End Property

    Private ReadOnly _Data As List(Of Byte)
    Public ReadOnly Property Data() As List(Of Byte)
        Get
            Return _Data
        End Get
    End Property
End Class

Public Class FlapTransportEventArgs
    Inherits EventArgs

    Private ReadOnly _Flap As Flap

    Public Sub New(ByVal f As Flap)
        _Flap = f
    End Sub

    Public ReadOnly Property Flap() As Flap
        Get
            Return _Flap
        End Get
    End Property
End Class
